
#include <limits>

#include <GL/glu.h>
#include <QGLWidget>
#include <QGraphicsSceneMouseEvent>
#include <QMutexLocker>
#include <QPainter>
#include <QPointF>
#include <QSet>
#include <QtOpenGL>
#include <QVector4D>

#include "ai/flock.h"
#include "core/engine.h"
#include "core/game.h"
#include "core/scene.h"
#include "core/view.h"
#include "entity/cow.h"
#include "geometry/quadtree.h"
#include "geometry/sphere.h"
#include "object/face.h"
#include "object/material.h"

Scene::Scene(QObject* pkParent)
    : QGraphicsScene(pkParent),
      m_pkUFO(NULL),
      m_pkFlock(NULL),
      m_uiRandomizerDelay(1000)
{
}

Scene::~Scene()
{
    delete m_pkFlock;
    delete m_pkUFO;

    UFO::object().unloadGPU();
    Cow::object().unloadGPU();
}

void Scene::initialize()
{
    // Randomization.
    m_kRandomizer.setInterval(m_uiRandomizerDelay);
    connect(&m_kRandomizer, SIGNAL(timeout()), this, SLOT(randomize()));

    //

    QGLWidget* pkViewport = static_cast< QGLWidget* >(Engine::instance().view().viewport());
    pkViewport->makeCurrent();

    setItemIndexMethod(QGraphicsScene::NoIndex);

    // Model loading.
    // Navmap.
    m_kNavMap.load(":/model/map/navmap.obj");
    Q_ASSERT(m_kNavMap.isValid());

    // Map.
    m_kMapObject.loadOBJ(":/model/map/map.obj");
    Q_ASSERT(m_kMapObject.isValid());

    m_kMapObject.loadGPU();
    new (&m_kMapEntity) Entity(m_kMapObject);

    UFO::object().loadOBJ(":/model/ufo/UFO.obj");
    Q_ASSERT(UFO::object().isValid());
    UFO::object().loadGPU();
    Q_ASSERT(UFO::object().isLoadedGPU());
    m_pkUFO = new UFO();

    Cow::object().loadOBJ(":/model/cow/cow.obj");
    Q_ASSERT(Cow::object().isValid());
    Cow::object().loadGPU();
    Q_ASSERT(Cow::object().isLoadedGPU());

    QMatrix4x4 kUFOMatrix = m_pkUFO->matrix();
    kUFOMatrix.scale(0.7);
    m_pkUFO->setMatrix(kUFOMatrix);

    pkViewport->doneCurrent();

    // Instanciate flock.
    m_pkFlock = new Flock(m_kNavMap);
}

void Scene::setup()
{
    // Clean everything.
    foreach(Agent* pkAgent, m_pkFlock->agents())
    {
        m_pkFlock->remove(*pkAgent);
        delete static_cast< Cow* >(pkAgent);
    }

    const QSet< const Face* >& rkWalkables = m_kNavMap.walkables().items();
    quint8 uiWalkableIndex = 0;
    const Face* pkWalkable = NULL;
    Game& rkGame = Engine::instance().game();

    // Randomly place UFO.
    uiWalkableIndex = qrand() % rkWalkables.count();
    pkWalkable = *(rkWalkables.begin() + uiWalkableIndex);
    QVector3D kUFOPosition = pkWalkable->barycenter();
    m_pkUFO->setPosition(kUFOPosition);

    // Generate & randomly place cows.
    for(quint8 uiCowIndex = 0; uiCowIndex < rkGame.cowCount(); ++uiCowIndex)
    {
        Cow* pkCow = new Cow(*m_pkFlock);
        Q_CHECK_PTR(pkCow);
        m_pkFlock->add(*pkCow);

        // Initialize.

        // This should be moved. It ensure that initial faces are not border faces.
        qreal distance = 0;
        while(distance < 0.2)
        {
            distance = std::numeric_limits< qreal >::max();

            // Get a random walkable
            uiWalkableIndex = qrand() % rkWalkables.count();
            pkWalkable = *(rkWalkables.begin() + uiWalkableIndex);

            const QVector< const QVector3D* >& rkVertices = pkWalkable->vertices();
            for(quint32 i = 0; i < rkVertices.count(); ++i)
            {
                quint32 j = (i + 1) % rkVertices.count();

                distance = qMin(distance, (*(rkVertices.at(i)) - *(rkVertices.at(j))).length());
            }
        }

        pkCow->setPosition(pkWalkable->barycenter() + QVector3D(0, Cow::object().height() / 2, 0));
        pkCow->setVelocity((qrand() % static_cast< quint32 >(Cow::s_maxVelocity) - qrand() % static_cast< quint32 >(Cow::s_maxVelocity)) / 5,
                           0,
                           (qrand() % static_cast< quint32 >(Cow::s_maxVelocity) - qrand() % static_cast< quint32 >(Cow::s_maxVelocity)) / 5);
        pkCow->setID(uiCowIndex / 2);
    }

    // Flock settings.
    m_pkFlock->setAlignementFactor(0.4);
    m_pkFlock->setAlignementFunction(Flock::Linear);
    m_pkFlock->setAttractionFactor(0.4);
    m_pkFlock->setAttractionFunction(Flock::Constant);
    m_pkFlock->setRepulsionFactor(0.4);
    m_pkFlock->setRepulsionFunction(Flock::Exponential);
    m_pkFlock->setDisturbanceType(Flock::Repulsive);
    m_pkFlock->setDisturbanceFactor(3);
    m_pkFlock->setDisturbanceFunction(Flock::Exponential);

    m_pkFlock->setContainmentFunction(Flock::Exponential);
    m_pkFlock->setContainmentRange(1.5);
    m_pkFlock->setContainmentFactor(25);

    // Start timers.
    m_kRandomizer.start();

    // Setup cameras.
    qreal cos = qCos(-M_PI / 4);
    qreal sin = qSin(-M_PI / 4);

    m_kGlobalViewCamera = QMatrix4x4(1, 0, 0, 10,
                                     0, cos, -sin, 100,
                                     0, sin, cos, 100,
                                     0, 0, 0, 1);

    m_kUFOViewCamera = QMatrix4x4(1, 0, 0, 10,
                                  0, cos, -sin, 20,
                                  0, sin, cos, 20,
                                  0, 0, 0, 1);
}

void Scene::step()
{
    // Set camera.
    if(Engine::instance().game().isRunning())
    {
        m_kUFOViewCamera.setColumn(3, QVector4D(m_pkUFO->position() + QVector3D(0, 20, 20), 1));

        m_kCamera = m_kUFOViewCamera * 0.7 + m_kGlobalViewCamera * 0.3;
    }
    else
    {
        qreal cos = qCos(-M_PI / 4);
        qreal sin = qSin(-M_PI / 4);

        m_kCamera = QMatrix4x4(1, 0, 0, 10,
                               0, cos, -sin, 40,
                               0, sin, cos, 30,
                               0, 0, 0, 1);
    }

    // Advance Flock and Move Agents.
    m_pkFlock->update();

    foreach(Agent* pkAgent, m_pkFlock->agents())
    {
        Cow* pkCow = static_cast< Cow* >(pkAgent);

        if(!pkCow->isActive())
            continue;

        if(pkCow->velocity().lengthSquared() < qPow(pkCow->s_minVelocity, 2))
            continue;

        // Integrate.
        pkCow->setVelocity(pkCow->velocity().normalized() * (pkCow->velocity().length() - Agent::s_braking * Engine::s_updateDelay));

        QVector3D kTarget = pkCow->position() +
                            pkCow->velocity() *
                            Engine::s_updateDelay;
        kTarget.setY(Cow::object().height() / 2);

        pkCow->setPosition(kTarget);

        // Set heading.
        qreal angle = qAcos(pkCow->velocity().x() / pkCow->velocity().length());
        angle = pkCow->velocity().z() > 0 ? angle : -angle;
        pkCow->setHeading(angle - M_PI / 2);
    }

    // Stop UFO move sound
    if(m_pkUFO->path().covered() == m_pkUFO->path().topologicalLength() || !m_pkUFO->path().isValid())
    {
        FMOD_RESULT kResult;
        FMOD_EVENT* pkFMODEventMoveUFO;
        kResult = FMOD_EventSystem_GetEvent(Engine::instance().fmod(), "melomaniac/melomaniac/move", FMOD_EVENT_DEFAULT, &pkFMODEventMoveUFO);
        Q_ASSERT(kResult == FMOD_OK);
        kResult = FMOD_Event_Stop(pkFMODEventMoveUFO, false);
        Q_ASSERT(kResult == FMOD_OK);
    }

    // Update mobile's position.
    if(!m_pkUFO->path().isValid())
        return;

    m_pkUFO->path().move(m_pkUFO->velocity() * Engine::s_updateDelay);

    QVector3D kPosition = m_pkUFO->path().position();
    kPosition.setY(5);
    m_pkUFO->setPosition(kPosition);

    kPosition.setY(Cow::object().height() / 2);
    m_pkFlock->setDisturbancePosition(kPosition);

    // Update teleported cow's position.
    foreach(Cow* pkCow, m_kTeleportedCows)
    {
        // Clamp position to UFO's position except for the Y.
        QVector3D kTarget = pkCow->position();
        kTarget.setX(m_pkUFO->position().x());
        kTarget.setZ(m_pkUFO->position().z());

        // Switch UFO state proceed to upward teleport or downward teleport.
        switch(m_pkUFO->state())
        {
            case UFO::Loading:
                // Update position.
                kTarget.setY(kTarget.y() + m_pkUFO->teleportVelocity() * Engine::s_updateDelay * 4);
                pkCow->setPosition(kTarget);

                break;

            case UFO::Unloading:
                // Update position.
                kTarget.setY(kTarget.y() - m_pkUFO->teleportVelocity() * Engine::s_updateDelay * 4);
                pkCow->setPosition(kTarget);
                break;
        }
    }

    // Check for loading and unloading.
    foreach(Cow* pkCow, m_kTeleportedCows)
    {
        QVector3D kTarget = pkCow->position();
        kTarget.setX(m_pkUFO->position().x());
        kTarget.setZ(m_pkUFO->position().z());

        switch(m_pkUFO->state())
        {
            case UFO::Loading:
                // Check if current cow has reached the UFO.
            if(Sphere::intersection(Sphere(m_pkUFO->body().center()), pkCow->body()))
                {
                    pkCow->setPosition(m_pkUFO->position());
                    pkCow->setVisible(false);

                    m_pkUFO->load(*pkCow);
                    m_kTeleportedCows.dequeue();

                    // Stop UFO teleport sound
                    if(m_kTeleportedCows.isEmpty())
                    {
                        FMOD_RESULT kResult;
                        FMOD_EVENT* pkFMODEventUFOTeleport;
                        kResult = FMOD_EventSystem_GetEvent(Engine::instance().fmod(), "melomaniac/melomaniac/teleport", FMOD_EVENT_DEFAULT, &pkFMODEventUFOTeleport);
                        Q_ASSERT(kResult == FMOD_OK);

                        kResult = FMOD_Event_Stop(pkFMODEventUFOTeleport, false);
                        Q_ASSERT(kResult == FMOD_OK);
                    }
                    // If the UFO is full : test loaded cow pair
                    if(m_pkUFO->isFull())
                    {
                        if(m_pkUFO->loadedCow(0).id() == m_pkUFO->loadedCow(1).id())
                        {
                            Engine::instance().game().addMatch();
                            m_pkUFO->clear();
                        }
                        else
                        {
                            m_pkUFO->setState(UFO::Unloading);
                        }
                    }
                }
                break;

            case UFO::Unloading:
                // Check if the current cow has reached the ground.
                if(pkCow->body().y() <= Cow::object().height() / 2)
                {
                    kTarget.setY(Cow::object().height() / 2);
                    pkCow->setPosition(kTarget);
                    pkCow->setActive(true);

                    m_kTeleportedCows.dequeue();

                    // Stop UFO teleport sound
                    if(m_kTeleportedCows.isEmpty())
                    {
                        FMOD_RESULT kResult;
                        FMOD_EVENT* pkFMODEventUFOTeleport;
                        kResult = FMOD_EventSystem_GetEvent(Engine::instance().fmod(), "melomaniac/melomaniac/teleport", FMOD_EVENT_DEFAULT, &pkFMODEventUFOTeleport);
                        Q_ASSERT(kResult == FMOD_OK);
                        kResult = FMOD_Event_Stop(pkFMODEventUFOTeleport, false);
                        Q_ASSERT(kResult == FMOD_OK);
                    }

                    // If the beam is empty then change state to loading.
                    if(!m_kTeleportedCows.count() && m_pkUFO->isEmpty())
                        m_pkUFO->setState(UFO::Loading);
                }
                break;
        }
    }

    // Try to teleport
    if(m_pkUFO->state() == UFO::Unloading && m_pkUFO->isTeleportReady() && !m_pkUFO->isEmpty())
    {
        // Play UFO teleport sound
        if(m_kTeleportedCows.isEmpty())
        {
            FMOD_RESULT kResult;
            FMOD_EVENT* pkFMODEventUFOTeleport;
            kResult = FMOD_EventSystem_GetEvent(Engine::instance().fmod(), "melomaniac/melomaniac/teleport", FMOD_EVENT_DEFAULT, &pkFMODEventUFOTeleport);
            Q_ASSERT(kResult == FMOD_OK);
            kResult = FMOD_Event_Start(pkFMODEventUFOTeleport);
            Q_ASSERT(kResult == FMOD_OK);
        }

        m_pkUFO->fire();
        Cow& rkCow = m_pkUFO->unload();
        m_kTeleportedCows.enqueue(&rkCow);
        rkCow.setVisible(true);
    }
}

void Scene::randomize()
{
    foreach(Agent* pkAgent, m_pkFlock->agents())
    {
        if(!pkAgent->isActive())
            continue;

        // Randomly start or stop agents based on velocities.
        qreal odds = qrand() % 100 / 100.0;

        if(pkAgent->velocity().isNull() && odds < Agent::s_startProbability)
        {
            pkAgent->setVelocity(qrand() % 100 / 100.0 * Agent::s_maxVelocity / 8,
                                 0,
                                 qrand() % 100 / 100.0 * Agent::s_maxVelocity / 8);
        }
        else if(odds * pkAgent->velocity().length() / Agent::s_maxVelocity < Agent::s_stopProbability)
        {
            pkAgent->setVelocity(0, 0, 0);
        }
    }
}

void Scene::drawBackground(QPainter* pkPainter, const QRectF& rkRect)
{
    Q_ASSERT(pkPainter->paintEngine()->type() == QPaintEngine::OpenGL2);

    // State setup.
    glPushAttrib(GL_ENABLE_BIT);
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);
    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);

    // Setup lighting.
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    GLfloat afPosition[] = { m_kMapEntity.matrix().column(3).x() / 2,
                             25,
                             m_kMapEntity.matrix().column(3).z() / 2};
    GLfloat afDirection[] = {0.f, -1.f, 0.f};
    glLightfv(GL_LIGHT0, GL_POSITION, afPosition);
    glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, afDirection);

    GLfloat afAmbiantColor[] = { 1.0, 1.0, 1.0, 1.0 };
    GLfloat afDiffuseColor[] = { 1.0, 1.0, 1.0, 1.0 };
    glLightfv(GL_LIGHT0, GL_AMBIENT, afAmbiantColor);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, afDiffuseColor);

    // Matrix.
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glFrustum(-2.0f, 2.0f, -1.0f, 1.0f, 10.0f, 500.0f);

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();

    glLoadMatrixd(m_kCamera.inverted().data());

    // Clearing & painting.
    glClearColor(0.5020f, 0.6314f, 0.1216f, 1.f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    m_kMapEntity.paint();
    //m_kNavMap.paint();
    if(Engine::instance().game().isRunning())
    {
        m_pkUFO->path().paint();
        m_pkUFO->paint();
    }
    //m_pkFlock->paint();

    foreach(Agent* pkAgent, m_pkFlock->agents())
    {
        Cow* pkCow = static_cast< Cow* >(pkAgent);
        pkCow->paint();
    }

    // Restoring.
    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
    glPopAttrib();
}

void Scene::mousePressEvent(QGraphicsSceneMouseEvent* pkEvent)
{
    // Propagate.
    QGraphicsScene::mousePressEvent(pkEvent);

    // Player clic input handling.
    if(!Engine::instance().game().isRunning())
        return;

    switch(pkEvent->buttons())
    {
    // Player is moving.
    case Qt::LeftButton:
    {
        GLint aiViewport[4];
        GLdouble adModelViewMatrix[16], adProjectionMatrix[16];

        // Initialization.
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glLoadIdentity();
        glFrustum(-2.0f, 2.0f, -1.0f, 1.0f, 10.0f, 500.0f);

        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();
        glLoadMatrixd(m_kCamera.inverted().data());

        glGetDoublev(GL_MODELVIEW_MATRIX, adModelViewMatrix);
        glGetDoublev(GL_PROJECTION_MATRIX, adProjectionMatrix);
        glGetIntegerv(GL_VIEWPORT, aiViewport);

        // Unproject the screen point.
        QPointF kScreenPoint = pkEvent->scenePos();
        GLdouble dBx, dBy, dBz;

        gluUnProject(kScreenPoint.x(), aiViewport[3] - kScreenPoint.y(), 0,
                     adModelViewMatrix, adProjectionMatrix, aiViewport, &dBx, &dBy, &dBz);

        // Compute intersection with the XZ plane.
        QVector3D kA = m_kCamera.column(3).toVector3D();
        QVector3D kB(dBx, dBy, dBz);
        GLdouble dT = -kA.y() / (dBy - kA.y());

        if(dT < 0)
            break;

        QVector3D kM = (kB - kA) * dT + kA;

        // Play UFO move sound
        FMOD_RESULT kResult;
        FMOD_EVENT* pkFMODEventMoveUFO;
        kResult = FMOD_EventSystem_GetEvent(Engine::instance().fmod(), "melomaniac/melomaniac/move", FMOD_EVENT_DEFAULT, &pkFMODEventMoveUFO);
        Q_ASSERT(kResult == FMOD_OK);
        kResult = FMOD_Event_Start(pkFMODEventMoveUFO);
        Q_ASSERT(kResult == FMOD_OK);

        // Call pathfinding.
        QVector3D kUFOProjectedPosition = m_pkUFO->position();
        kUFOProjectedPosition.setY(0);
        m_pkUFO->path() = m_kNavMap.findPath(kUFOProjectedPosition, kM);
        break;
    }
    // Player is trying to teleport.
    case Qt::RightButton:
    {
        // Check if teleport is allowed.
        // Teleport is ready, not full and UFO is currently in loading state.
        if(!m_pkUFO->isTeleportReady() || m_kTeleportedCows.count() == 2 || m_pkUFO->state() != UFO::Loading)
            break;

        // Find relevant quadrant.
        QVector3D kUFOProjectedPosition = m_pkUFO->position();
        kUFOProjectedPosition.setY(0);
        const Sphere kTeleportSphere(kUFOProjectedPosition, m_pkUFO->teleportRadius());

        const Quadtree< Sphere >* pkQuadrant = m_pkFlock->ranges().findItem(kTeleportSphere);

        Q_ASSERT(pkQuadrant);

        // Find the closest cow.
        qreal closest = std::numeric_limits< qreal >::max();
        Cow* pkCow = NULL;

        foreach(const Sphere* pkRange, pkQuadrant->items())
        {
            QVector3D kIntersection;
            if(Sphere::intersection(kTeleportSphere, Sphere(pkRange->center()), &kIntersection))
            {
                qreal distance = (kIntersection - kTeleportSphere.center()).length();

                if(distance < closest)
                {
                    closest = distance;
                    Agent* pkAgent = static_cast< Agent* >(pkRange->userData());
                    pkCow = static_cast< Cow* >(pkAgent);
                }
            }
        }

        // Teleport the closest cow.
        if(!pkCow)
            break;

        // Play UFO teleport sound
        if(m_kTeleportedCows.isEmpty())
        {
            FMOD_RESULT kResult;
            FMOD_EVENT* pkFMODEventUFOTeleport;
            kResult = FMOD_EventSystem_GetEvent(Engine::instance().fmod(), "melomaniac/melomaniac/teleport", FMOD_EVENT_DEFAULT, &pkFMODEventUFOTeleport);
            Q_ASSERT(kResult == FMOD_OK);
            kResult = FMOD_Event_Start(pkFMODEventUFOTeleport);
            Q_ASSERT(kResult == FMOD_OK);
        }

        m_pkUFO->fire();
        pkCow->setActive(false);
        m_kTeleportedCows.enqueue(pkCow);

        // Notity teleport to Game
        Engine::instance().game().addShot();

        // Play teleported cow sound
        FMOD_RESULT kResult;
        FMOD_EVENT* pkFMODEventCow;
        FMOD_EVENTPARAMETER* pkFMODEventParameterCow;
        kResult = FMOD_EventSystem_GetEvent(Engine::instance().fmod(), "melomaniac/melomaniac/moo", FMOD_EVENT_DEFAULT, &pkFMODEventCow);
        Q_ASSERT(kResult == FMOD_OK);
        kResult = FMOD_Event_GetParameter(pkFMODEventCow, "variant", &pkFMODEventParameterCow);
        Q_ASSERT(kResult == FMOD_OK);
        kResult = FMOD_EventParameter_SetValue(pkFMODEventParameterCow, pkCow->id());
        Q_ASSERT(kResult == FMOD_OK);
        kResult = FMOD_Event_Start(pkFMODEventCow);
        Q_ASSERT(kResult == FMOD_OK);
    }
    }
}
